/*
 * This file is part of the Micro Python project, http://micropython.org/
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 Paul Sokolovsky
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>

#include "py/nlr.h"
#include "py/obj.h"
#include "py/gc.h"
#include "py/runtime.h"
#include "netutils.h"
#include "queue.h"
#include "user_interface.h"
#include "espconn.h"
#include "spi_flash.h"
#include "utils.h"

#include "os_timer.h"
#include "os_task.h"
#include "mod_esp_gpio.h"
#include "mod_esp_dht.h"
#include "mod_esp_mutex.h"
#include "esp_i2c_master.h"
#include "mod_esp_I2C.h"
#include "mod_esp_1wire.h"
#include "mod_esp_queue.h"
#include "mod_esp_smartconfig.h"
#include "mod_esp_wifi_events.h"
#include "mod_esp_ws.h"

#define MODESP_ESPCONN (0)

#if MODESP_ESPCONN

STATIC const mp_obj_type_t esp_socket_type;

typedef struct _esp_socket_obj_t {
    mp_obj_base_t base;
    struct espconn *espconn;

    mp_obj_t cb_connect;
    mp_obj_t cb_recv;
    mp_obj_t cb_sent;
    mp_obj_t cb_disconnect;
    mp_obj_t cb_reconnect;

    uint8_t *recvbuf;
    mp_uint_t recvbuf_len;

    bool fromserver;

    mp_obj_list_t *connlist;
} esp_socket_obj_t;

// Due to the onconnect callback not being able to recognize the parent esp_socket,
// we can have only one esp_socket listening at a time
// This should be solvable by some PIC hacking
STATIC esp_socket_obj_t *esp_socket_listening;

STATIC mp_obj_t esp_socket_make_new_base() {
    esp_socket_obj_t *s = m_new_obj_with_finaliser(esp_socket_obj_t);
    s->recvbuf = NULL;
    s->base.type = (mp_obj_t)&esp_socket_type;
    s->cb_connect = mp_const_none;
    s->cb_recv = mp_const_none;
    s->cb_disconnect = mp_const_none;
    s->cb_reconnect = mp_const_none;
    s->cb_sent = mp_const_none;
    s->fromserver = false;
    s->connlist = NULL;
    return s;
}

// constructor esp_socket(family=AF_INET, type=SOCK_STREAM, proto=IPPROTO_TCP, fileno=None)
// Arguments ignored as we do not support UDP (yet)
STATIC mp_obj_t esp_socket_make_new(const mp_obj_type_t *type_in, mp_uint_t n_args,
    mp_uint_t n_kw, const mp_obj_t *args) {
    mp_arg_check_num(n_args, n_kw, 0, 4, false);

    esp_socket_obj_t *s = esp_socket_make_new_base();
    s->espconn = m_new_obj(struct espconn);

    s->espconn->reverse = s;
    // TODO: UDP Support
    s->espconn->type = ESPCONN_TCP;
    s->espconn->state = ESPCONN_NONE;

    s->espconn->proto.tcp = m_new_obj(esp_tcp);

    return s;
}

// method socket.close()
STATIC mp_obj_t esp_socket_close(mp_obj_t self_in) {
    esp_socket_obj_t *s = self_in;

    if (esp_socket_listening == s) {
        gc_free(esp_socket_listening);
        esp_socket_listening = NULL;
        printf("sock listening == s, gc_free");
    } else {
        printf("sock listening != s, gc_free");
    }

    if (s->espconn->state != ESPCONN_NONE && s->espconn->state != ESPCONN_CLOSE) {
        espconn_disconnect(s->espconn);
    }

    if (s->connlist != NULL) {
        printf("connlist set len 0");
        mp_obj_list_set_len(s->connlist, 0);
    }
    s->recvbuf = NULL;
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(esp_socket_close_obj, esp_socket_close);

// method socket.__del__()
STATIC mp_obj_t esp_socket___del__(mp_obj_t self_in) {
    esp_socket_obj_t *s = self_in;

    esp_socket_close(self_in);

    printf("__del__ deleted\n");
    if (s->fromserver) {
        printf("server deleted\n");
        espconn_delete(s->espconn);
    }

    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(esp_socket___del___obj, esp_socket___del__);

// method socket.bind(address)
STATIC mp_obj_t esp_socket_bind(mp_obj_t self_in, mp_obj_t addr_in) {
    esp_socket_obj_t *s = self_in;

    mp_uint_t port = netutils_parse_inet_addr(addr_in,
        s->espconn->proto.tcp->remote_ip, NETUTILS_BIG);
    s->espconn->proto.tcp->local_port = port;

    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp_socket_bind_obj, esp_socket_bind);

STATIC void esp_socket_recv_callback(void *arg, char *pdata, unsigned short len) {
    struct espconn *conn = arg;
    esp_socket_obj_t *s = conn->reverse;

    if (s->cb_recv != mp_const_none) {
        printf("new bytes\n");
        call_function_2_protected(s->cb_recv, s, mp_obj_new_bytes((byte *)pdata, len));
    } else {
        if (s->recvbuf == NULL) {
            printf("recv null \n");
            s->recvbuf = m_new(uint8_t, len);
            s->recvbuf_len = len;
            if (s->recvbuf != NULL) {
                memcpy(s->recvbuf, pdata, len);
            } else {
                printf("failed alloc 1\n");
            }
        } else {
            s->recvbuf = m_renew(uint8_t, s->recvbuf, s->recvbuf_len, s->recvbuf_len + len);
            if (s->recvbuf != NULL) {
                memcpy(&s->recvbuf[s->recvbuf_len], pdata, len);
                s->recvbuf_len += len;
            } else {
                printf("failed alloc 3\n");
            }
        }
        if (s->recvbuf == NULL) {
            printf("close 2\n");
            esp_socket_close(s);
            return;
        }
    }
}

STATIC void esp_socket_sent_callback(void *arg) {
    struct espconn *conn = arg;
    esp_socket_obj_t *s = conn->reverse;

    if (s->cb_sent != mp_const_none) {
        call_function_1_protected(s->cb_sent, s);
    }
}

STATIC void esp_socket_disconnect_callback(void *arg) {
    struct espconn *conn = arg;
    esp_socket_obj_t *s = conn->reverse;

    if (s->cb_disconnect != mp_const_none) {
        call_function_1_protected(s->cb_disconnect, s);
    }
    s->recvbuf = NULL;
    esp_socket_close(s);
}

STATIC void esp_socket_reconnect_callback(void *arg, sint8 err) {
    struct espconn *conn = arg;
    esp_socket_obj_t *s = conn->reverse;

    if (s->cb_reconnect != mp_const_none) {
        call_function_1_protected(s->cb_reconnect, s);
    }
    s->recvbuf = NULL;
    esp_socket_close(s);
}

STATIC void esp_socket_connect_callback_server(void *arg) {
    struct espconn *conn = arg;

    esp_socket_obj_t *s = esp_socket_make_new_base();
    s->espconn = conn;
    s->fromserver = true;
    conn->reverse = s;

    espconn_regist_recvcb(conn, esp_socket_recv_callback);
    espconn_regist_sentcb(conn, esp_socket_sent_callback);
    espconn_regist_disconcb(conn, esp_socket_disconnect_callback);
    espconn_regist_reconcb(conn, esp_socket_reconnect_callback);
    espconn_regist_time(conn, 15, 0);

    if (esp_socket_listening->cb_connect != mp_const_none) {
        call_function_1_protected(esp_socket_listening->cb_connect, s);
    } else {
        mp_obj_list_append(esp_socket_listening->connlist, s);
    }
}

STATIC void esp_socket_connect_callback_client(void *arg) {
    struct espconn *conn = arg;
    esp_socket_obj_t *s = conn->reverse;

    if (s->cb_connect != mp_const_none) {
        call_function_1_protected(s->cb_connect, s);
    }
}

// method socket.listen(backlog)
STATIC mp_obj_t esp_socket_listen(mp_obj_t self_in, mp_obj_t backlog) {
    esp_socket_obj_t *s = self_in;

    if (esp_socket_listening != NULL) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError,
            "only one espconn can listen at a time"));
    }

    esp_socket_listening = s;

    s->connlist = mp_obj_new_list(0, NULL);

    espconn_regist_connectcb(s->espconn, esp_socket_connect_callback_server);
    espconn_accept(s->espconn);
    espconn_regist_time(s->espconn, 1500, 0);

    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp_socket_listen_obj, esp_socket_listen);

// method socket.accept()
STATIC mp_obj_t esp_socket_accept(mp_obj_t self_in) {
    esp_socket_obj_t *s = self_in;

    if (s->connlist == NULL) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError,
            "not listening"));
    }

    do {
        mp_uint_t len;
        mp_obj_t *items;

        mp_obj_list_get(s->connlist, &len, &items);
        if (len == 0) {
            break;
        }

        esp_socket_obj_t *rs = items[0];
        mp_obj_list_remove(s->connlist, rs);
        if (rs->espconn->state != ESPCONN_CLOSE) {
            return rs;
        }
    } while (true);

    nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError,
        "no connection in queue"));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(esp_socket_accept_obj, esp_socket_accept);

// method socket.connect(address)
STATIC mp_obj_t esp_socket_connect(mp_obj_t self_in, mp_obj_t addr_in) {
    esp_socket_obj_t *s = self_in;

    if (s->espconn == NULL || (s->espconn->state != ESPCONN_CLOSE && s->espconn->state != ESPCONN_NONE)) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError,
            "transport endpoint is already connected or closed"));
    }
    espconn_regist_connectcb(s->espconn, esp_socket_connect_callback_client);
    espconn_regist_recvcb(s->espconn, esp_socket_recv_callback);
    espconn_regist_sentcb(s->espconn, esp_socket_sent_callback);
    espconn_regist_disconcb(s->espconn, esp_socket_disconnect_callback);
    espconn_regist_reconcb(s->espconn, esp_socket_reconnect_callback);

    s->espconn->proto.tcp->remote_port =
        netutils_parse_inet_addr(addr_in, s->espconn->proto.tcp->remote_ip,
            NETUTILS_BIG);
    espconn_connect(s->espconn);

    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp_socket_connect_obj, esp_socket_connect);

// method socket.send(bytes)
STATIC mp_obj_t esp_socket_send(mp_obj_t self_in, mp_obj_t buf_in) {
    esp_socket_obj_t *s = self_in;

    if (s->espconn->state == ESPCONN_NONE || s->espconn->state == ESPCONN_CLOSE) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError,
            "not connected"));
    }

    mp_buffer_info_t bufinfo;
    mp_get_buffer_raise(buf_in, &bufinfo, MP_BUFFER_READ);

    int32_t err;
    if ((err = espconn_sent(s->espconn, bufinfo.buf, bufinfo.len)) !=  ESPCONN_OK) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError, "espconn_sent Not OK"));
    }

    return mp_obj_new_int(bufinfo.len);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp_socket_send_obj, esp_socket_send);

// method socket.recv(bufsize)
STATIC mp_obj_t esp_socket_recv(mp_obj_t self_in, mp_obj_t len_in) {
    esp_socket_obj_t *s = self_in;

    if (s->recvbuf == NULL) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError,
            "no data available"));
    }

    mp_uint_t mxl = mp_obj_get_int(len_in);
    if (mxl >= s->recvbuf_len) {
        mp_obj_t trt = mp_obj_new_bytes(s->recvbuf, s->recvbuf_len);
        m_del(uint8_t, s->recvbuf, s->recvbuf_len);
        s->recvbuf = NULL;
        return trt;
    } else {
        mp_obj_t trt = mp_obj_new_bytes(s->recvbuf, mxl);
        memmove(s->recvbuf, &s->recvbuf[mxl], s->recvbuf_len - mxl);
        s->recvbuf = m_renew(uint8_t, s->recvbuf, s->recvbuf_len, s->recvbuf_len - mxl);
        s->recvbuf_len -= mxl;
        return trt;
    }
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp_socket_recv_obj, esp_socket_recv);

// method socket.sendto(bytes, address)
STATIC mp_obj_t esp_socket_sendto(mp_obj_t self_in, mp_obj_t data_in, mp_obj_t addr_in) {
    nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError, "UDP not supported"));
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_3(esp_socket_sendto_obj, esp_socket_sendto);

// method socket.recvfrom(bufsize)
STATIC mp_obj_t esp_socket_recvfrom(mp_obj_t self_in, mp_obj_t len_in) {
    nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError, "UDP not supported"));
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp_socket_recvfrom_obj, esp_socket_recvfrom);

// method socket.getpeername()
STATIC mp_obj_t esp_socket_getpeername(mp_obj_t self_in) {
    esp_socket_obj_t *s = self_in;

    if (s->espconn->state == ESPCONN_NONE) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError,
            "not connected"));
    }

    mp_obj_t tuple[2] = {
        netutils_format_ipv4_addr(s->espconn->proto.tcp->remote_ip, NETUTILS_BIG),
        mp_obj_new_int(s->espconn->proto.tcp->remote_port),
    };

    return mp_obj_new_tuple(2, tuple);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(esp_socket_getpeername_obj, esp_socket_getpeername);

STATIC mp_obj_t esp_socket_onconnect(mp_obj_t self_in, mp_obj_t lambda_in) {
    esp_socket_obj_t *s = self_in;
    s->cb_connect = lambda_in;

    if (s->connlist != NULL) {
        do {
            mp_uint_t len;
            mp_obj_t *items;

            mp_obj_list_get(s->connlist, &len, &items);
            if (len == 0) {
                break;
            }

            esp_socket_obj_t *rs = items[0];
            mp_obj_list_remove(s->connlist, rs);
            if (s->espconn->state != ESPCONN_CLOSE) {
                call_function_1_protected(s->cb_connect, rs);
            }
        } while (true);
    }

    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp_socket_onconnect_obj, esp_socket_onconnect);

STATIC mp_obj_t esp_socket_onrecv(mp_obj_t self_in, mp_obj_t lambda_in) {
    esp_socket_obj_t *s = self_in;
    s->cb_recv = lambda_in;
    if (s->recvbuf != NULL) {
        call_function_2_protected(s->cb_recv, s,
            mp_obj_new_bytes((byte *)s->recvbuf, s->recvbuf_len));
        s->recvbuf = NULL;
    }
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp_socket_onrecv_obj, esp_socket_onrecv);

STATIC mp_obj_t esp_socket_onsent(mp_obj_t self_in, mp_obj_t lambda_in) {
    esp_socket_obj_t *s = self_in;
    s->cb_sent = lambda_in;
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp_socket_onsent_obj, esp_socket_onsent);

STATIC mp_obj_t esp_socket_ondisconnect(mp_obj_t self_in, mp_obj_t lambda_in) {
    esp_socket_obj_t *s = self_in;
    s->cb_disconnect = lambda_in;

    if (s->espconn->state == ESPCONN_CLOSE) {
        call_function_1_protected(s->cb_disconnect, s);
    }

    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp_socket_ondisconnect_obj, esp_socket_ondisconnect);

STATIC mp_obj_t esp_socket_onreconnect(mp_obj_t self_in, mp_obj_t lambda_in) {
    esp_socket_obj_t *s = self_in;
    s->cb_reconnect = lambda_in;
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp_socket_onreconnect_obj, esp_socket_onreconnect);

STATIC mp_obj_t esp_socket_state(mp_obj_t self_in) {
    esp_socket_obj_t *s = self_in;
    if (s->espconn == NULL)
        return mp_const_none;
    else
        return mp_obj_new_int(s->espconn->state);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(esp_socket_state_obj, esp_socket_state);


STATIC const mp_map_elem_t esp_socket_locals_dict_table[] = {
    { MP_OBJ_NEW_QSTR(MP_QSTR___del__), (mp_obj_t)&esp_socket___del___obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_close), (mp_obj_t)&esp_socket_close_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_bind), (mp_obj_t)&esp_socket_bind_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_listen), (mp_obj_t)&esp_socket_listen_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_accept), (mp_obj_t)&esp_socket_accept_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_connect), (mp_obj_t)&esp_socket_connect_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_send), (mp_obj_t)&esp_socket_send_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_recv), (mp_obj_t)&esp_socket_recv_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_sendto), (mp_obj_t)&esp_socket_sendto_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_recvfrom), (mp_obj_t)&esp_socket_recvfrom_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_getpeername), (mp_obj_t)&esp_socket_getpeername_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_onconnect), (mp_obj_t)&esp_socket_onconnect_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_onrecv), (mp_obj_t)&esp_socket_onrecv_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_onsent), (mp_obj_t)&esp_socket_onsent_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_ondisconnect), (mp_obj_t)&esp_socket_ondisconnect_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_onreconnect), (mp_obj_t)&esp_socket_onreconnect_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_state), (mp_obj_t)&esp_socket_state_obj }
};
STATIC MP_DEFINE_CONST_DICT(esp_socket_locals_dict, esp_socket_locals_dict_table);

STATIC const mp_obj_type_t esp_socket_type = {
    { &mp_type_type },
    .name = MP_QSTR_socket,
    .make_new = esp_socket_make_new,
    .locals_dict = (mp_obj_t)&esp_socket_locals_dict,
};
#endif

typedef struct _esp_getaddrinfo_cb_struct_t {
    mp_obj_t lambda;
    mp_uint_t port;
} esp_getaddrinfo_cb_struct_t;

STATIC esp_getaddrinfo_cb_struct_t esp_getaddrinfo_cb_struct;

STATIC void esp_getaddrinfo_cb(const char *name, ip_addr_t *ipaddr, void *arg) {
    mp_obj_t namestr = mp_obj_new_str(name, strlen(name), true);
    if (ipaddr != NULL) {
        uint8_t ip[4];
        ip[0] = (ipaddr->addr >> 24) & 0xff;
        ip[1] = (ipaddr->addr >> 16) & 0xff;
        ip[2] = (ipaddr->addr >>  8) & 0xff;
        ip[3] = (ipaddr->addr >>  0) & 0xff;

        mp_obj_tuple_t *tuple = mp_obj_new_tuple(5, NULL);

        tuple->items[0] = MP_OBJ_NEW_SMALL_INT(0);
        tuple->items[1] = MP_OBJ_NEW_SMALL_INT(0);
        tuple->items[2] = MP_OBJ_NEW_SMALL_INT(0);
        tuple->items[3] = MP_OBJ_NEW_QSTR(MP_QSTR_);
        tuple->items[4] = netutils_format_inet_addr(ip,
            esp_getaddrinfo_cb_struct.port, NETUTILS_LITTLE);
        call_function_2_protected(esp_getaddrinfo_cb_struct.lambda, namestr, tuple);
    } else {
        call_function_2_protected(esp_getaddrinfo_cb_struct.lambda, namestr, mp_const_none);
    }
}

STATIC mp_obj_t esp_getaddrinfo(mp_obj_t host_in, mp_obj_t port_in,
    mp_obj_t lambda_in) {
    mp_uint_t hlen;
    const char *host = mp_obj_str_get_data(host_in, &hlen);
    ip_addr_t ipaddr;

    esp_getaddrinfo_cb_struct.lambda = lambda_in;
    esp_getaddrinfo_cb_struct.port = mp_obj_get_int(port_in);

    err_t ret = espconn_gethostbyname(NULL, host, &ipaddr,
        esp_getaddrinfo_cb);

    if (ret == ESPCONN_OK) {
        esp_getaddrinfo_cb(host, &ipaddr, NULL);
    }

    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_3(esp_getaddrinfo_obj, esp_getaddrinfo);

#define MODESP_INCLUDE_CONSTANTS (1)

void error_check(bool status, const char *msg) {
    if (!status) {
        nlr_raise(mp_obj_new_exception_msg(&mp_type_OSError, msg));
    }
}

STATIC mp_obj_t esp_wifi_mode(mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args == 0) {
        return mp_obj_new_int(wifi_get_opmode());
    } else {
        wifi_set_opmode(mp_obj_get_int(args[0]));
        return mp_const_none;
    }
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(esp_wifi_mode_obj, 0, 1, esp_wifi_mode);

STATIC mp_obj_t esp_phy_mode(mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args == 0) {
        return mp_obj_new_int(wifi_get_phy_mode());
    } else {
        wifi_set_phy_mode(mp_obj_get_int(args[0]));
        return mp_const_none;
    }
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(esp_phy_mode_obj, 0, 1, esp_phy_mode);

STATIC mp_obj_t esp_sleep_type(mp_uint_t n_args, const mp_obj_t *args) {
    if (n_args == 0) {
        return mp_obj_new_int(wifi_get_sleep_type());
    } else {
        wifi_set_sleep_type(mp_obj_get_int(args[0]));
        return mp_const_none;
    }
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(esp_sleep_type_obj, 0, 1, esp_sleep_type);

STATIC mp_obj_t esp_deepsleep(mp_uint_t n_args, const mp_obj_t *args) {
    system_deep_sleep(n_args > 0 ? mp_obj_get_int(args[0]) : 0);
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(esp_deepsleep_obj, 0, 1, esp_deepsleep);

uint32_t fs_size()
{
    uint32_t id = spi_flash_get_id();  
    printf("id %x\n", id);
    uint8_t mfgr_id = id & 0xff;
    uint8_t size_id = (id >> 16) & 0xff; 
    if(mfgr_id != 0xEF) {
        printf("Manufacturer %x\n", mfgr_id);
        return 0;
    }
    return 1 << size_id;
}
STATIC mp_obj_t esp_flash_id() {
    //return mp_obj_new_int(spi_flash_get_id());
    return mp_obj_new_int(fs_size());
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(esp_flash_id_obj, esp_flash_id);



// STATIC mp_obj_t ICACHE_FLASH_ATTR esp__init__() {
STATIC mp_obj_t esp__init__() {
    esp_os_task_init();
    esp_gpio_init();
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(esp__init__obj, esp__init__);

STATIC mp_obj_t esp_test(mp_obj_t obj, mp_obj_t obj2) {
    extern int rofl(const char *ss, const int arg2);
    mp_uint_t len;
    const char *ss = mp_obj_str_get_data(obj, &len);

    rofl(ss, mp_obj_get_int(obj2));
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp_test_obj, esp_test);

STATIC mp_obj_t esp_flash_read(mp_obj_t offset_in, mp_obj_t len_in) {
    mp_int_t offset = mp_obj_get_int(offset_in);
    mp_int_t len = mp_obj_get_int(len_in);
    byte *buf = m_new(byte, len);
    // We know that allocation will be 4-byte aligned for sure
    SpiFlashOpResult res = spi_flash_read(offset, (uint32_t*)buf, len);
    if (res == SPI_FLASH_RESULT_OK) {
        return mp_obj_new_bytes(buf, len);
    }
    m_del(byte, buf, len);
    nlr_raise(mp_obj_new_exception_arg1(&mp_type_OSError, MP_OBJ_NEW_SMALL_INT(res == SPI_FLASH_RESULT_TIMEOUT ? ETIMEDOUT : EIO)));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(esp_flash_read_obj, esp_flash_read);


STATIC const mp_map_elem_t esp_module_globals_table[] = {
    { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_esp) },

    { MP_OBJ_NEW_QSTR(MP_QSTR_wifi_mode), (mp_obj_t)&esp_wifi_mode_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_phy_mode), (mp_obj_t)&esp_phy_mode_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_sleep_type), (mp_obj_t)&esp_sleep_type_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_deepsleep), (mp_obj_t)&esp_deepsleep_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_flash_id), (mp_obj_t)&esp_flash_id_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_flash_read), (mp_obj_t)&esp_flash_read_obj },
#if MODESP_ESPCONN
    { MP_OBJ_NEW_QSTR(MP_QSTR_socket), (mp_obj_t)&esp_socket_type },
#endif
    { MP_OBJ_NEW_QSTR(MP_QSTR_os_timer), (mp_obj_t)&esp_os_timer_type },
    { MP_OBJ_NEW_QSTR(MP_QSTR_os_task), (mp_obj_t)&esp_os_task_type },
    { MP_OBJ_NEW_QSTR(MP_QSTR_gpio), (mp_obj_t)&mp_module_esp_gpio },
#if MICROPY_MODULE_ESP_DHT
    { MP_OBJ_NEW_QSTR(MP_QSTR_dht), (mp_obj_t)&esp_dht_type },
#endif
    { MP_OBJ_NEW_QSTR(MP_QSTR_mutex), (mp_obj_t)&esp_mutex_type },
    { MP_OBJ_NEW_QSTR(MP_QSTR___init__), (mp_obj_t)&esp__init__obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_test), (mp_obj_t)&esp_test_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_I2C), (mp_obj_t)&esp_I2C_type },
#if MICROPY_MODULE_ESP_1WIRE
    { MP_OBJ_NEW_QSTR(MP_QSTR_one_wire), (mp_obj_t)&esp_1wire_type },
#endif
    { MP_OBJ_NEW_QSTR(MP_QSTR_queue), (mp_obj_t)&esp_queue_type },
    { MP_OBJ_NEW_QSTR(MP_QSTR_smartconfig), (mp_obj_t)&mp_module_esp_smartconfig},
    { MP_OBJ_NEW_QSTR(MP_QSTR_wifi_events), (mp_obj_t)&mp_module_esp_wifi_events},
    { MP_OBJ_NEW_QSTR(MP_QSTR_getaddrinfo), (mp_obj_t)&esp_getaddrinfo_obj },
    { MP_OBJ_NEW_QSTR(MP_QSTR_ws), (mp_obj_t)&esp_ws_type },

#if MODESP_INCLUDE_CONSTANTS
    { MP_OBJ_NEW_QSTR(MP_QSTR_MODE_11B),
        MP_OBJ_NEW_SMALL_INT(PHY_MODE_11B) },
    { MP_OBJ_NEW_QSTR(MP_QSTR_MODE_11G),
        MP_OBJ_NEW_SMALL_INT(PHY_MODE_11G) },
    { MP_OBJ_NEW_QSTR(MP_QSTR_MODE_11N),
        MP_OBJ_NEW_SMALL_INT(PHY_MODE_11N) },

    { MP_OBJ_NEW_QSTR(MP_QSTR_SLEEP_NONE),
        MP_OBJ_NEW_SMALL_INT(NONE_SLEEP_T) },
    { MP_OBJ_NEW_QSTR(MP_QSTR_SLEEP_LIGHT),
        MP_OBJ_NEW_SMALL_INT(LIGHT_SLEEP_T) },
    { MP_OBJ_NEW_QSTR(MP_QSTR_SLEEP_MODEM),
        MP_OBJ_NEW_SMALL_INT(MODEM_SLEEP_T) },

    { MP_OBJ_NEW_QSTR(MP_QSTR_STA_MODE),
        MP_OBJ_NEW_SMALL_INT(STATION_MODE)},
    { MP_OBJ_NEW_QSTR(MP_QSTR_AP_MODE),
        MP_OBJ_NEW_SMALL_INT(SOFTAP_MODE)},
    { MP_OBJ_NEW_QSTR(MP_QSTR_STA_AP_MODE),
        MP_OBJ_NEW_SMALL_INT(STATIONAP_MODE)},
    { MP_OBJ_NEW_QSTR(MP_QSTR_STA_MODE),
        MP_OBJ_NEW_SMALL_INT(STATION_MODE)},

     { MP_OBJ_NEW_QSTR(MP_QSTR_ESPCONN_NONE), MP_OBJ_NEW_SMALL_INT(ESPCONN_NONE)},
     { MP_OBJ_NEW_QSTR(MP_QSTR_ESPCONN_WAIT), MP_OBJ_NEW_SMALL_INT(ESPCONN_WAIT)},
     { MP_OBJ_NEW_QSTR(MP_QSTR_ESPCONN_CLOSE), MP_OBJ_NEW_SMALL_INT(ESPCONN_CLOSE)},
     { MP_OBJ_NEW_QSTR(MP_QSTR_ESPCONN_CONNECT), MP_OBJ_NEW_SMALL_INT(ESPCONN_CONNECT)},
     { MP_OBJ_NEW_QSTR(MP_QSTR_ESPCONN_WRITE), MP_OBJ_NEW_SMALL_INT(ESPCONN_WRITE)},
     { MP_OBJ_NEW_QSTR(MP_QSTR_ESPCONN_READ), MP_OBJ_NEW_SMALL_INT(ESPCONN_READ)},
     { MP_OBJ_NEW_QSTR(MP_QSTR_ESPCONN_CLOSE), MP_OBJ_NEW_SMALL_INT(ESPCONN_CLOSE)}


#endif
};

STATIC MP_DEFINE_CONST_DICT(esp_module_globals, esp_module_globals_table);

const mp_obj_module_t esp_module = {
    .base = { &mp_type_module },
    .name = MP_QSTR_esp,
    .globals = (mp_obj_dict_t*)&esp_module_globals,
};
